﻿using AzureVMDashboard.UI;
using Microsoft.WindowsAzure.Commands.ServiceManagement.Model;
using Persitent = Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVMModel;
using Microsoft.WindowsAzure.Commands.Utilities.Common;
using Microsoft.WindowsAzure.ServiceManagement;
//using Microsoft.WindowsAzure.Management.Model;
//using Microsoft.WindowsAzure.Management.ServiceManagement.Model;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Xml;


namespace AzureVMDashboard.Model
{
    /// <remarks>Singleton</remarks>
    public class AzureVMController : ModelBase
    {
        private static volatile AzureVMController instance;
        private static object syncRoot = new Object();

        private DirectoryInfo WorkingFolder;

        private AzureVMController(DirectoryInfo workingFolder)
        {
            ControllerInitialized = false;
            IsWorking = false;
            AzureSubscriptions = new ObservableCollection<AzureSubscription>();

            FileInfo azureModule = new FileInfo(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + @"\Microsoft SDKs\Windows Azure\PowerShell\Azure\Azure.psd1");
            if (azureModule.Exists)
                AzureModulePath = azureModule;
            else
                Logger.LogEntry(LogType.Verbose, "Azure PS Module not found at: " + azureModule.FullName);

            if (workingFolder.Exists)
                WorkingFolder = workingFolder;
            else
                Logger.LogEntry(LogType.Warning, "Working folder not exists: " + workingFolder.FullName);


        }

        public static AzureVMController GetInstance(DirectoryInfo workingDir)
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                        instance = new AzureVMController(workingDir);
                }
            }

            return instance;
        }


        #region Properties
        /// <summary>
        /// File generated from Get-AzurePublishSettingsFile, contains a management certificate and all subscriptions
        /// </summary>   
        public FileInfo PublishSettingsFile
        {
            get;
            private set;
        }

        public FileInfo AzureModulePath
        {
            get;
            set;
        }

        public bool ControllerInitialized;



        public virtual ObservableCollection<AzureSubscription> AzureSubscriptions
        {
            get;
            set;
        }

        #endregion

        public async Task StartAllAsync(AzureSubscription subscription)
        {
            await Task.Factory.StartNew(() =>
            {

                Logger.LogEntry(LogType.Info, "Starting all VMs");

                //barrier used to disabled UI until we are working
                Barrier stopWorkingBarrier = new Barrier(subscription.VMs.Count, null);
                IsWorking = true;
                Parallel.ForEach<AzureVM>(subscription.VMs, async vm =>
                {
                    vm.StartAsync().Wait();
                    stopWorkingBarrier.SignalAndWait();
                });
                IsWorking = false;
                Logger.LogEntry(LogType.Info, "VMs started successfully");

            });
        }

        //public async Task StartAll(AzureSubscription subscription)
        //{
        //    Logger.LogEntry(LogType.Info, "Starting all VMs");
        //    IsWorking = true;
        //    await Task.Factory.StartNew(() =>
        //    {
        //        try
        //        {
        //            //barrier used to disabled UI until we are working
        //            //Barrier stopWorkingBarrier = new Barrier(subscription.VMs.Count,
        //            //    (b) => IsWorking = false);

        //            using (PowerShell ps = LoadAzureModuleSettings(subscription))
        //            {
        //                foreach (AzureVM vm in subscription.VMs)

        //                // List<AzureVM> currentVMs = subscription.VMs.ToList<AzureVM>();

        //                //Parallel.ForEach<AzureVM>(currentVMs, async vm =>
        //                {
        //                    try
        //                    {
        //                        ps.AddCommand("Start-AzureVM");
        //                        ps.AddParameter("ServiceName", vm.ServiceName);
        //                        ps.AddParameter("Name", vm.Name);
        //                        LogVerboseCommands(ps);

        //                        foreach (PSObject result in ps.Invoke())
        //                        {
        //                            ManagementOperationContext res = (ManagementOperationContext)result.BaseObject;
        //                            if (res.OperationStatus == "Succeeded")
        //                                vm.Status = VMStatus.Running;
        //                            else
        //                            {
        //                                vm.Error = "Cannot start the VM";
        //                                vm.Status = VMStatus.Error;
        //                            }
        //                        }
        //                    }
        //                    catch (Exception pse)
        //                    {
        //                        Logger.LogEntry("Could not start " + vm.Name, pse);
        //                        throw new Exception("Start failed");
        //                    }
        //                    finally
        //                    {
        //                        //  stopWorkingBarrier.SignalAndWait();
        //                    }
        //                    //});
        //                };

        //                CheckIfPSHasError(ps);
        //            }
        //        }
        //        catch (Exception pse)
        //        {
        //            Logger.LogEntry("Start All failed", pse);
        //        }
        //        finally
        //        {
        //            IsWorking = false;
        //        }

        //    });


        //    Logger.LogEntry(LogType.Info, "VMs started successfully");
        //}

        public async void StopAllAsync(AzureSubscription subscription)
        {

            await Task.Factory.StartNew(() =>
            {

                Logger.LogEntry(LogType.Info, "Stopping all VMs");

                //barrier used to disabled UI until we are working
                Barrier stopWorkingBarrier = new Barrier(subscription.VMs.Count, null);
                IsWorking = true;
                Parallel.ForEach<AzureVM>(subscription.VMs, async vm =>
                {
                    vm.StopAsync().Wait();
                    stopWorkingBarrier.SignalAndWait();
                });
                IsWorking = false;
                Logger.LogEntry(LogType.Info, "VMs stopped successfully");

            });

        }

        public async Task StartVMAsync(AzureVM vm)
        {
            if (!ControllerInitialized)
            {
                Logger.LogEntry(LogType.Error, "Powershell not ready");
                return;
            }

            Logger.LogEntry(LogType.Info, "Starting " + vm.Name);

#if OFFLINE
            await Task.Delay(5000);
            vm.Status = VMStatus.Running;
            return;
#else
            await Task.Factory.StartNew(() =>
            {
                try
                {

                    IsWorking = true;
                    using (PowerShell ps = LoadAzureModuleSettings(vm.Subscription))
                    {
                        vm.Status = VMStatus.Updating;

                        ps.AddCommand("Start-AzureVM");
                        ps.AddParameter("ServiceName", vm.ServiceName);
                        ps.AddParameter("Name", vm.Name);
                        LogVerboseCommands(ps);

                        foreach (PSObject result in ps.Invoke())
                        {
                            ManagementOperationContext res = (ManagementOperationContext)result.BaseObject;
                            if (res.OperationStatus == "Succeeded")
                                vm.Status = VMStatus.Running;
                            else
                            {
                                vm.Error = "Cannot start the VM";
                                vm.Status = VMStatus.Error;
                            }
                        }

                        CheckIfPSHasError(ps);
                    }

                }
                catch (Exception pse)
                {
                    Logger.LogEntry("Could not start " + vm.Name, pse);
                    throw new Exception("Start failed");
                }
                finally
                {
                    IsWorking = false;
                }
            });


#endif
        }

        public async Task StopVMAsync(AzureVM vm)
        {
            if (!ControllerInitialized)
            {
                Logger.LogEntry(LogType.Warning, "Powershell not ready");
                return;
            }

            Logger.LogEntry(LogType.Info, "Stopping " + vm.Name);

#if OFFLINE
            await Task.Delay(5000);
            vm.Status = VMStatus.Off;
#else
            await Task.Factory.StartNew(() =>
           {
               try
               {

                   IsWorking = true;

                   using (PowerShell ps = LoadAzureModuleSettings(vm.Subscription))
                   {


                       vm.Status = VMStatus.Updating;
                       ps.AddCommand("Stop-AzureVM");
                       ps.AddParameter("ServiceName", vm.ServiceName);
                       ps.AddParameter("Name", vm.Name);
                       ps.AddParameter("StayProvisioned");
                       LogVerboseCommands(ps);

                       foreach (PSObject result in ps.Invoke())
                       {
                           ManagementOperationContext res = (ManagementOperationContext)result.BaseObject;
                           if (res.OperationStatus == "Succeeded")
                               vm.Status = VMStatus.Off;
                           else
                           {
                               vm.Error = "Cannot stop the VM";
                               vm.Status = VMStatus.Error;
                           }
                       }

                       CheckIfPSHasError(ps);
                   }

               }
               catch (Exception pse)
               {
                   Logger.LogEntry("Could not stop " + vm.Name, pse);
                   throw new Exception("Stop failed");
               }
               finally
               {
                   IsWorking = false;
               }
           });
#endif
        }



        public async Task GetDetailedInfoAsync(AzureVM vm)
        {
            if (!ControllerInitialized)
            {
                Logger.LogEntry(LogType.Warning, "Powershell not ready");
                return;
            }

            Logger.LogEntry(LogType.Info, String.Format("Downloading detailed info for \"{0}\"", vm.Name));

            await Task.Factory.StartNew(() =>
            {
                try
                {
                    IsWorking = true;
                    using (PowerShell ps = LoadAzureModuleSettings(vm.Subscription))
                    {

                        //1 command for all machines
                        ps.AddCommand("Get-AzureVM");
                        ps.AddParameter("ServiceName", vm.ServiceName);
                        ps.AddParameter("Name", vm.Name);
                        LogVerboseCommands(ps);

                        foreach (PSObject result in ps.Invoke())
                        {
                            PersistentVM onlineVMobject = ((PersistentVMRoleContext)result.BaseObject).VM;
                            PersistentVMRoleContext onlineVMContext = (PersistentVMRoleContext)result.BaseObject;

                            vm.Size = onlineVMContext.InstanceSize;
                            vm.LocalIPAddress = IPAddress.Parse(onlineVMContext.IpAddress);

                            vm.FQDN = ((PersistentVMRoleContext)result.BaseObject).DNSName;
                            vm.FQDN = vm.FQDN.Substring(7, vm.FQDN.Length - 7 - 1); //removing HTTP:// + last char (/)

                            if (String.IsNullOrEmpty(vm.FQDN))
                            {
                                Logger.LogEntry(LogType.Warning, "No Dns name for " + vm.Name);
                                break;
                            }


                            //search for any RDP endpoint
                            if (onlineVMobject.ConfigurationSets != null && onlineVMobject.ConfigurationSets.Count > 0)
                            {
                                foreach (Persitent.ConfigurationSet cs in onlineVMobject.ConfigurationSets)
                                {
                                    if (cs is Persitent.NetworkConfigurationSet)
                                    {
                                        Persitent.NetworkConfigurationSet ncs = cs as Persitent.NetworkConfigurationSet;
                                        foreach (Persitent.InputEndpoint ie in ncs.InputEndpoints)
                                        {
                                            if (ie.LocalPort == 3389)
                                            {
                                                vm.RdpPort = ie.Port.Value;
                                                Logger.LogEntry(LogType.Verbose, "Found RDP endpoint for " + vm.Name);
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        CheckIfPSHasError(ps);

                    }


                    Logger.LogEntry(LogType.Info, "Details for " + vm.Name + " downloaded");

                }
                catch (Exception pse)
                {
                    Logger.LogEntry("Could not retrieve detailed info for " + vm.Name, pse);
                }
                finally
                {
                    IsWorking = false;
                }
            }
            );


        }

        public async Task RetrieveVMsAsync(AzureSubscription subscription)
        {
            if (!ControllerInitialized)
            {
                Logger.LogEntry(LogType.Warning, "Powershell not ready");
                return;
            }

            if (subscription == null)
            {
                Logger.LogEntry(LogType.Warning, "No subscription select");
                return;
            }

            Logger.LogEntry(LogType.Info, String.Format("Downloading VMs for \"{0}\"",subscription.Name));

#if OFFLINE
            await Task.Delay(5000);
            for (int i = 0; i < 10; i++)
            {
                AzureVM vm = new AzureVM(subscription);
                vm.Name = "VM " + i;
                vm.FQDN = "vm.cloudapp.net";
                vm.ServiceName = vm.Name + "Service";
                vm.Size = "Small";
                vm.LocalIPAddress = IPAddress.Parse("192.168.1.1");
                vm.RdpPort = 3389;

                vm.Status = VMStatus.Running;
                subscription.VMs.Add(vm);
            }
            //return;
#else


            await Task.Factory.StartNew(() =>
            {
                try
                {
                    IsWorking = true;
                    //1 command for all machines
                    using (PowerShell ps = LoadAzureModuleSettings(subscription))
                    {

                        ps.AddCommand("Get-AzureVM");
                        LogVerboseCommands(ps);
                        foreach (PSObject result in ps.Invoke())
                        {
                            PersistentVMRoleListContext vmInfo = (PersistentVMRoleListContext)result.BaseObject;
                            AzureVM vm = new AzureVM(subscription);
                            vm.Name = vmInfo.Name;
                            vm.ServiceName = vmInfo.ServiceName;
                            Logger.LogEntry(LogType.Info, String.Format("Found VM: {0} ({1})", vm.Name, vm.ServiceName));
                            switch (vmInfo.Status)
                            {
                                case "StoppedVM": { vm.Status = VMStatus.Off; break; }
                                case "ReadyRole": { vm.Status = VMStatus.Running; break; }
                                default: { vm.Status = VMStatus.Updating; break; }
                            }

                            if (!subscription.VMs.Contains(vm))
                            {
                                Application.Current.Dispatcher.Invoke(() => subscription.VMs.Add(vm)); //because VMs are bound to UI
                                GetDetailedInfoAsync(vm);
                            }
                        }
                        CheckIfPSHasError(ps);
                    }

                    Logger.LogEntry(LogType.Info, "Refresh completed");
                }
                catch (Exception pse)
                {
                    Logger.LogEntry("Could not retrieve VMs", pse);
                }
                finally
                {
                    IsWorking = false;
                }
            }
            );
#endif

        }

        /// <summary>
        /// Check if the Powershell finished with errors, if so Throw an Exception
        /// </summary>
        private void CheckIfPSHasError(PowerShell ps)
        {
            if (ps.HadErrors)
            {
                Logger.LogEntry(LogType.Error, "PS command failed: " + ps.Commands.Commands[0].CommandText);

                foreach (ErrorRecord er in ps.Streams.Error)
                {
                    Logger.LogEntry(LogType.Verbose, "ErrorID: " + er.FullyQualifiedErrorId);
                    Logger.LogEntry(LogType.Verbose, "Category: " + er.CategoryInfo);
                    Logger.LogEntry(LogType.Verbose, "Error details: " + er.ErrorDetails);
                    Logger.LogEntry(LogType.Verbose, "Exception: " + er.Exception.Message);
                    if (er.Exception != null)
                        throw er.Exception;
                }

            }
        }


        /// <returns>True is Azure PS CmdLets work</returns>
        public bool CheckIfAzurePSIsInstalled()
        {

            if (AzureModulePath == null || !File.Exists(AzureModulePath.FullName))
                throw new Exception("Azure PS module not valid");

            try
            {
                using (PowerShell ps = PowerShell.Create())
                {
                    ps.AddCommand("Import-Module");
                    ps.AddParameter("Name", AzureModulePath.FullName);
                    LogVerboseCommands(ps);
                    ps.Invoke();
                    CheckIfPSHasError(ps);
                }
            }
            catch (Exception pse)
            {
                Logger.LogEntry("Azure PowerShell not installed", pse);
                return false;
            }

            return true;
        }

        public bool RemoveSubscription(AzureSubscription subscription)
        {
            bool result = false;
            try
            {
                Logger.LogEntry(LogType.Verbose, String.Format("Removing Azure Subscription: \"{0}\"",subscription.Name));

                using (PowerShell ps = PowerShell.Create())
                {
                    ps.AddCommand("Import-Module");
                    ps.AddParameter("Name", AzureModulePath.FullName);
                    LogVerboseCommands(ps);
                    ps.Invoke();
                    ps.Commands.Clear();

                    ps.AddCommand("Remove-AzureSubscription"); //could not exists
                    ps.AddParameter("SubscriptionName", subscription.Name);
                    ps.AddParameter("Force");
                    LogVerboseCommands(ps);
                    ps.Invoke();

                    CheckIfPSHasError(ps);
                    result = true;
                    Logger.LogEntry(LogType.Verbose, String.Format("Removed Azure Subscription: \"{0}\"",subscription.Name));

                }
            }
            catch (Exception pse)
            {
                Logger.LogEntry("Could not remove Azure subscription", pse);
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Check if Azure Powershell commands work, and discover all the subscriptions in the publishing settings file
        /// Then registers all the Subscriptions specified the file publishing settings file into the current user profile
        /// Each subscription will then be available for the user
        /// </summary>
        /// <param name="publishSettingsFile">File generated by Get-AzurePublishSettingsFile, contains all the subscirptions for the user and the management certificate</param>
        /// <returns>false in case of errors</returns>
        public bool InitializeController(FileInfo publishSettingsFile)
        {
            //STEPS
            // 1- import the azure module to check if works
            // 2- Initialize each subscription


            bool result = false;
            Logger.LogEntry(LogType.Info, "Initializing Azure PS Controller");
            IsWorking = true;

            try
            {

                if (!CheckIfAzurePSIsInstalled())
                    return false;

                if (publishSettingsFile == null || !publishSettingsFile.Exists)
                    throw new Exception("Publish Settings file not valid");

                PublishSettingsFile = publishSettingsFile; //file exists!
                Logger.LogEntry(LogType.Verbose, "Publish Settings File: " + PublishSettingsFile.FullName);

                //remove any subscription previously loaded
                //to avoid duplicated name (i.e: when user change publising file)
                foreach (AzureSubscription s in AzureSubscriptions)
                {
                    RemoveSubscription(s);
                }
                AzureSubscriptions.Clear();

                using (PowerShell ps = PowerShell.Create())
                {
                    ps.AddCommand("Import-Module");
                    ps.AddParameter("Name", AzureModulePath.FullName);
                    LogVerboseCommands(ps);
                    ps.Invoke();
                    ps.Commands.Clear();

                    DiscoverSubscriptions(); //discover Azure Subscriptions and add them to the main collection
                    if (AzureSubscriptions.Count > 0)
                    {
                        Logger.LogEntry(LogType.Info, "Initializing Azure Subscriptions");
                        //the following command registers all the Subscriptions specified the file into the current machine
                        //the file contains also the management certificate
                        //each subscription will then be available for the user, to switch between subscription each command must specify the subscription name (unique)
                        ps.AddCommand("Import-AzurePublishSettingsFile");
                        ps.AddParameter("PublishSettingsFile", PublishSettingsFile.FullName);
                        LogVerboseCommands(ps);
                        ps.Invoke();
                        Logger.LogEntry(LogType.Info, "Azure Subscriptions Initialized!");
                    }
                    CheckIfPSHasError(ps);
                }

                Logger.LogEntry(LogType.Info, "Azure PS Controller Initialized!");
                result = true;
            }
            catch (Exception pse)
            {
                Logger.LogEntry("Could not initiliaze the Azure PS Controller", pse);
                result = false;
            }
            finally
            {
                ControllerInitialized = result;
                IsWorking = false;
            }

            return result;
        }

        /// <summary>
        /// Goes through the Publish settings file to find out all the Subscriptions, it adds the results to the Subscriptions collection
        /// </summary>
        private void DiscoverSubscriptions()
        {

            XmlDocument doc = new XmlDocument();
            doc.Load(PublishSettingsFile.FullName);

            var subNodes = doc.SelectNodes("//Subscription");
            foreach (XmlElement s in subNodes)
            {

                AzureSubscription newAS = new AzureSubscription(this);
                newAS.ID = s.Attributes["Id"].Value;
                newAS.Name = s.Attributes["Name"].Value;

                Logger.LogEntry(LogType.Info, String.Format("Found Azure Subscription: \"{0}\"", newAS.Name));
                if (!AzureSubscriptions.Contains(newAS))
                    AzureSubscriptions.Add(newAS);
            }
        }

        /// <summary>
        /// Creates a PS object and initialize the environment for Azure Cmdlets, select the given subscription as current Azure subscription.
        /// To be called before to execute Azure PS commands
        /// </summary>
        private PowerShell LoadAzureModuleSettings(AzureSubscription subscription)
        {
            PowerShell ps = PowerShell.Create();

            ps.AddCommand("Import-Module");
            ps.AddParameter("Name", AzureModulePath.FullName);
            LogVerboseCommands(ps);
            ps.Invoke();
            ps.Commands.Clear();

            ps.AddCommand("Select-AzureSubscription");
            ps.AddParameter("SubscriptionName", subscription.Name.Trim());
            LogVerboseCommands(ps);
            ps.Invoke();
            ps.Commands.Clear();

            return ps;
        }

        /// <summary>
        /// Execute the PS commands that create a file with all the Subscriptions and the management certificate
        /// This file is needed to execute the PS Azure commands
        /// </summary>
        public void DownloadPublishSettings()
        {
            try
            {
                Logger.LogEntry(LogType.Info, "Downloading Azure Publish Settings - Waiting for User");

                if (AzureModulePath == null || !AzureModulePath.Exists)
                    throw new Exception("Azure PS module path not found");

                using (PowerShell ps = PowerShell.Create())
                {
                    ps.AddCommand("Import-Module");
                    ps.AddParameter("Name", AzureModulePath.FullName);
                    LogVerboseCommands(ps);
                    ps.Invoke();
                    ps.Commands.Clear();

                    ps.AddCommand("Get-AzurePublishSettingsFile");
                    LogVerboseCommands(ps);
                    ps.Invoke();

                    CheckIfPSHasError(ps);
                }
            }
            catch (Exception pse)
            {
                Logger.LogEntry("Could not Download Azure publish settings", pse);
            }
        }

        private void LogVerboseCommands(PowerShell ps)
        {
            foreach (Command c in ps.Commands.Commands)
            {
                StringBuilder runningCmd = new StringBuilder(c.CommandText);
                foreach (CommandParameter p in c.Parameters)
                {
                    runningCmd.Append(string.Format(" -{0} \"{1}\"", p.Name, p.Value));
                }
                Logger.LogEntry(LogType.Command, "Executing " + runningCmd);
            }
        }

    }
}

